package com.ewei.support.ldap;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

import org.budo.support.assertion.Assert;
import org.budo.support.lang.util.BooleanUtil;
import org.budo.support.lang.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPAttributeSet;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPJSSESecureSocketFactory;
import com.novell.ldap.LDAPSearchConstraints;
import com.novell.ldap.LDAPSearchResults;

/**
 * @author ZRP
 * @version 1 (2015年7月28日下午2:29:40)
 * @since Java7
 */
public class LdapUtil {
    private static final Logger log = LoggerFactory.getLogger(LdapUtil.class);

    private static String[] userAttributes = { "associatedDomain", "associatedName", "attributeTypes", "audio", "authorityRevocationList", "bootFile", "bootParameter", "buildingName", "businessCategory", "c", "cACertificate", "carLicense", "certificateRevocationList", "cn", "co", "crossCertificatePair", "dc", "deltaRevocationList", "departmentNumber", "description", "destinationIndicator", "displayName", "distinguishedName", "dmdName", "documentAuthor", "documentIdentifier", "documentLocation", "documentPublisher", "documentTitle", "documentVersion", "drink", "employeeNumber", "employeeType", "facsimileTelephoneNumber", "gecos", "generationQualifier", "gidNumber", "givenName", "homeDirectory", "homePhone", "homePostalAddress", "host", "houseIdentifier", "info", "initials", "ipHostNumber", "ipNetmaskNumber", "ipNetworkNumber", "ipProtocolNumber", "ipServicePort", "ipServiceProtocol", "knowledgeInformation", "l", "labeledURI", "loginShell", "macAddress", "mail", "manager", "member",
            "memberNisNetgroup", "memberUid", "mobile", "name", "nisMapEntry", "nisMapName", "nisNetgroupTriple", "o", "objectClass", "objectClasses", "oncRpcNumber", "organizationalStatus", "otherMailbox", "ou", "owner", "pager", "personalTitle", "photo", "physicalDeliveryOfficeName", "postalAddress", "postalCode", "postOfficeBox", "preferredDeliveryMethod", "preferredLanguage", "presentationAddress", "registeredAddress", "roleOccupant", "roomNumber", "searchGuide", "secretary", "seeAlso", "serialNumber", "shadowExpire", "shadowFlag", "shadowInactive", "shadowLastChange", "shadowMax", "shadowMin", "shadowWarning", "sn", "st", "street", "streetAddress", "structuralObjectClass", "supportedApplicationContext", "telephoneNumber", "teletexTerminalIdentifier", "telexNumber", "title", "textEncodedORAddress", "department", "uid", "uidNumber", "uniqueIdentifier", "uniqueMember", "userClass", "x121Address", "memberOf", "sAMAccountName", "wWWHomePage", "ipPhone", "company", "userPrincipalName",
            "entryDN", "entryUUID", "dcpersonalarea", "dcposition", "dcpersonalsubarea" };

    private static String ldapDomainName(String userDirectory) {
        Assert.notEmpty(userDirectory, "userDirectory 不能为空");

        String[] nodes = StringUtil.split(userDirectory, ",");
        String domainName = "";
        if (nodes.length > 0) {
            for (String node : nodes) {
                String[] nodeName = StringUtil.split(node, "=");
                if (nodeName.length >= 2) {
                    if ("dc".equalsIgnoreCase(nodeName[0])) {
                        if (StringUtil.isEmpty(domainName)) {
                            domainName += (nodeName[1]);
                        } else {
                            domainName += ("." + nodeName[1]);
                        }
                    }
                }
            }
        }
        return domainName;
    }

    /**
     * 登录ldap
     * 
     * @param username
     * @param password
     * @param serverName
     * @param port
     * @param isSsl
     * @return
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static LdapContext getConnection(String serverType, String userDirectory, String username, String password, String serverName, Integer port, Boolean isSsl) {
        if (password != null) {
            password = password.trim();
            if (password.length() == 0) {
                password = null;
            }
        }
        String principalName = username;
        if ("ad".equalsIgnoreCase(serverType) && (username.indexOf("\\") < 0 && username.indexOf("@") < 0)) {
            String ldapDomainName = ldapDomainName(userDirectory);
            if (!StringUtil.isEmpty(ldapDomainName)) {
                principalName = (username + "@" + ldapDomainName);
            }
        }
        Hashtable props = new Hashtable();

        props.put(Context.SECURITY_PRINCIPAL, principalName);
        props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        if (password != null) {
            props.put(Context.SECURITY_CREDENTIALS, password);
        }
        props.put(Context.SECURITY_AUTHENTICATION, "simple");
        String portString = "";
        if (port != null && port > 0) {
            portString = ":" + port;
        } else {
            if (BooleanUtil.isTrue(isSsl)) {
                portString = ":636";
            }
        }
        String ldapURL = "ldap://" + serverName + portString + "/";

        if (BooleanUtil.isTrue(isSsl)) {
            ldapURL = "ldaps://" + serverName + portString + "/";
            props.put(Context.SECURITY_PROTOCOL, "ssl");
            props.put("java.naming.ldap.factory.socket", LdapSSLSocketFactory.class.getName());
        }
        props.put(Context.PROVIDER_URL, ldapURL);

        try {
            return new InitialLdapContext(props, null);
        } catch (javax.naming.CommunicationException e) {
            log.error("getConnection() CommunicationException " + e, e);
            return null;
        } catch (NamingException e) {
            log.error("getConnection() NamingException " + e, e);
            return null;
        }
    }

    /**
     * 查询用户
     * 
     * @param username
     * @param context
     * @param userAttributes
     * @param pFitter
     */
    private static Attributes getUserByUsername(String serverType, String username, LdapContext context, String userDirectory, String pFitter) {
        try {
            if (context == null || StringUtil.isBlank(userDirectory) || StringUtil.isBlank(username)) {
                return null;
            }

            String principalName = username;
            if ("ad".equalsIgnoreCase(serverType) && (username.indexOf("\\") < 0 && username.indexOf("@") < 0)) {
                String ldapDomainName = ldapDomainName(userDirectory);
                if (!StringUtil.isEmpty(ldapDomainName)) {
                    principalName = (username + "@" + ldapDomainName);
                }
            }
            SearchControls controls = new SearchControls();
            controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            controls.setReturningAttributes(userAttributes);
            controls.setCountLimit(1);
            String baseFitter = "(|(objectClass=account)(objectClass=user)(objectClass=person)(objectClass=inetOrgPerson))";
            if (!StringUtil.isBlank(pFitter)) {
                baseFitter = pFitter;
            }
            String fitter = "(&(|(entryDN=" + principalName + ")(sAMAccountName=" + principalName + ")(userPrincipalName=" + principalName + ")(distinguishedName=" + principalName + ")(cn=" + principalName + ")(uid=" + principalName + ")(name=" + principalName + "))" + baseFitter + ")";
            NamingEnumeration<SearchResult> answer = context.search(userDirectory, fitter, controls);
            while (answer.hasMore()) {
                Attributes attr = answer.next().getAttributes();
                Attribute user = attr.get("cn");
                if (user != null) {
                    return attr;
                }
            }
        } catch (NamingException e) {
            log.error("getUser() NamingException " + e, e);
        } finally {
            closeLdapContext(context);
        }
        return null;
    }

    private static void closeLdapContext(LdapContext ldapContext) {
        if (null == ldapContext) {
            log.error("closeLdapContext error, ldapContext is null, ldapContext=" + ldapContext);
        }
        try {
            ldapContext.close();
        } catch (NamingException e) {
            log.error("context.close() NamingException " + e, e);
        }
    }

    public static Boolean ldapLogin(LdapLoginBean ldapLoginBean, String username, String password) {
        if (LdapLoginBean.SERVICE_TYPE_AD.equals(ldapLoginBean.getServerType())) {
            LdapContext ldapContext = getConnection(ldapLoginBean.getServerType(), ldapLoginBean.getUserDirectory(), username, password, ldapLoginBean.getServerName(), ldapLoginBean.getPort(), ldapLoginBean.getIsSsl());
            if (null != ldapContext) {
                return true;
            }
        } else {
            LDAPConnection ldapConnection = null;
            Integer port = ldapLoginBean.getPort();
            if (BooleanUtil.isTrue(ldapLoginBean.getIsSsl())) {
                ldapConnection = new LDAPConnection(new LDAPJSSESecureSocketFactory());
                if (null == port) {
                    port = LDAPConnection.DEFAULT_SSL_PORT;
                }
            } else {
                ldapConnection = new LDAPConnection();
                if (null == port) {
                    port = LDAPConnection.DEFAULT_PORT;
                }
            }

            if (null == password) {
                password = "";
            }

            try {
                String userDn = null;
                ldapConnection.connect(ldapLoginBean.getServerName(), port);
                ldapConnection.bind(LDAPConnection.LDAP_V3, ldapLoginBean.getAdminUsername(), ldapLoginBean.getAdminPassword().getBytes("UTF8"));

                boolean loginSuccess = checkLdapDnAndPassword(ldapLoginBean, username, password);
                if (!loginSuccess) {
                    String baseFitter = "(|(objectClass=account)(objectClass=user)(objectClass=person)(objectClass=inetOrgPerson))";
                    if (!StringUtil.isBlank(ldapLoginBean.getFitter())) {
                        baseFitter = ldapLoginBean.getFitter();
                    }
                    StringBuffer fitterBuffer = new StringBuffer();
                    if (isMailAddress(username)) {
                        fitterBuffer.append("(&(|(mail=");
                    } else {
                        fitterBuffer.append("(&(|(uid=");
                    }
                    fitterBuffer.append(username).append("))").append(baseFitter).append(")");

                    LDAPSearchConstraints lsc = new LDAPSearchConstraints();
                    lsc.setMaxResults(1);
                    LDAPSearchResults searchResults = ldapConnection.search(ldapLoginBean.getUserDirectory(), LDAPConnection.SCOPE_SUB, fitterBuffer.toString(), userAttributes, false, lsc);
                    LDAPEntry nextEntry = null;

                    while (searchResults.hasMore()) {
                        nextEntry = searchResults.next();
                        userDn = nextEntry.getDN();
                        return checkLdapDnAndPassword(ldapLoginBean, userDn, password);
                    }
                }
                return loginSuccess;

            } catch (UnsupportedEncodingException e) {
                log.error("ldapLogin UnsupportedEncodingException e=" + e, e);
                return false;
            } catch (LDAPException e) {
                log.error("ldapLogin LDAPException e=" + e, e);
                return false;
            }
        }
        return false;
    }

    private static boolean isMailAddress(String mailAddress) {
        if (StringUtil.isEmpty(mailAddress) || mailAddress.length() > 127) {
            return false;
        }
        String[] strs = mailAddress.split("\\@");
        if (strs.length < 2) {
            return false;
        }
        String[] subStrs = strs[1].split("\\.");
        if (subStrs.length < 2) {
            return false;
        }
        return true;
    }

    public static Attributes getUserByUserName(LdapLoginBean ldapLoginBean, String username) {
        if (LdapLoginBean.SERVICE_TYPE_AD.equals(ldapLoginBean.getServerType())) {
            LdapContext ldapContext = getConnection(ldapLoginBean.getServerType(), //
                    ldapLoginBean.getUserDirectory(), //
                    ldapLoginBean.getAdminUsername(), //
                    ldapLoginBean.getAdminPassword(), //
                    ldapLoginBean.getServerName(), //
                    ldapLoginBean.getPort(), //
                    ldapLoginBean.getIsSsl());

            if (null == ldapContext) {
                return null;
            }

            return getUserByUsername(ldapLoginBean.getServerType(), //
                    username, //
                    ldapContext, //
                    ldapLoginBean.getUserDirectory(), //
                    ldapLoginBean.getFitter());
        } else {
            LDAPConnection ldapConnection = null;
            Integer port = ldapLoginBean.getPort();
            if (BooleanUtil.isTrue(ldapLoginBean.getIsSsl())) {
                ldapConnection = new LDAPConnection(new LDAPJSSESecureSocketFactory());
                if (null == port) {
                    port = LDAPConnection.DEFAULT_SSL_PORT;
                }
            } else {
                ldapConnection = new LDAPConnection();
                if (null == port) {
                    port = LDAPConnection.DEFAULT_PORT;
                }
            }
            String password = ldapLoginBean.getAdminPassword();
            if (null == password) {
                password = "";
            }

            try {
                ldapConnection.connect(ldapLoginBean.getServerName(), port);
                ldapConnection.bind(LDAPConnection.LDAP_V3, ldapLoginBean.getAdminUsername(), password.getBytes("UTF8"));
                String baseFitter = "(|(objectClass=account)(objectClass=user)(objectClass=person)(objectClass=inetOrgPerson))";
                if (!StringUtil.isBlank(ldapLoginBean.getFitter())) {
                    baseFitter = ldapLoginBean.getFitter();
                }

                String fitter = "(&(|(mail=" + username + ")(uid=" + username + "))" + baseFitter + ")";
                LDAPSearchConstraints lsc = new LDAPSearchConstraints();
                lsc.setMaxResults(1);
                LDAPSearchResults searchResults = ldapConnection.search(ldapLoginBean.getUserDirectory(), LDAPConnection.SCOPE_SUB, fitter, userAttributes, false, lsc);
                List<Attributes> attributes = getAttributesFromSearchResults(searchResults);
                if (null == attributes || attributes.isEmpty()) {
                    log.error("查询用户信息时没有找到, username={}的用户,fitter={}", username, fitter);
                    return null;
                } else {
                    return attributes.get(0);
                }
            } catch (Throwable e) {
                log.error("getUserByUserName error=" + e, e);
            } finally {
                disconnectLdapConnection(ldapConnection);
            }
        }
        return null;
    }

    public static List<Attributes> getUserList(LdapLoginBean ldapLoginBean) {
        List<Attributes> attributes = null;

        LDAPConnection ldapConnection = null;
        Integer port = ldapLoginBean.getPort();
        if (BooleanUtil.isTrue(ldapLoginBean.getIsSsl())) {
            ldapConnection = new LDAPConnection(new LDAPJSSESecureSocketFactory());
            if (null == port) {
                port = LDAPConnection.DEFAULT_SSL_PORT;
            }
        } else {
            ldapConnection = new LDAPConnection();
            if (null == port) {
                port = LDAPConnection.DEFAULT_PORT;
            }
        }
        String password = ldapLoginBean.getAdminPassword();
        if (null == password) {
            password = "";
        }

        try {
            ldapConnection.connect(ldapLoginBean.getServerName(), port);
            ldapConnection.bind(LDAPConnection.LDAP_V3, ldapLoginBean.getAdminUsername(), password.getBytes("UTF8"));
            String baseFitter = "(|(objectClass=account)(objectClass=user)(objectClass=person)(objectClass=inetOrgPerson))";
            if (!StringUtil.isBlank(ldapLoginBean.getFitter())) {
                baseFitter = ldapLoginBean.getFitter();
            }

            LDAPSearchConstraints ldapSearchConstraints = new LDAPSearchConstraints();
            ldapSearchConstraints.setMaxResults(0);
            LDAPSearchResults searchResults = ldapConnection.search(ldapLoginBean.getUserDirectory(), LDAPConnection.SCOPE_SUB, baseFitter, userAttributes, false, ldapSearchConstraints);
            attributes = getAttributesFromSearchResults(searchResults);
        } catch (Throwable e) {
            log.error("getUserByUserName error=" + e, e);
        } finally {
            disconnectLdapConnection(ldapConnection);
        }
        return attributes;
    }

    private static void disconnectLdapConnection(LDAPConnection ldapConnection) {
        if (null == ldapConnection) {
            log.error("disconnectLdapConnection error, ldapConnection is null");
            return;
        }
        try {
            ldapConnection.disconnect();
        } catch (LDAPException e) {
            log.error("disconnectLdapConnection error, e=" + e, e);
        }
    }

    @SuppressWarnings("rawtypes")
    private static List<Attributes> getAttributesFromSearchResults(LDAPSearchResults searchResults) {
        Attributes attributes = null;
        List<Attributes> attributesList = new ArrayList<Attributes>();
        try {
            LDAPEntry nextEntry = null;
            boolean valueValid = false;
            while (searchResults.hasMore()) {
                valueValid = false;
                attributes = new BasicAttributes();
                nextEntry = searchResults.next();
                LDAPAttributeSet attributeSet = nextEntry.getAttributeSet();
                Iterator allAttributes = attributeSet.iterator();
                while (allAttributes.hasNext()) {
                    LDAPAttribute ldapAttribute = (LDAPAttribute) allAttributes.next();
                    String attributeName = ldapAttribute.getName();
                    Enumeration allValues = ldapAttribute.getStringValues();
                    if (null == allValues) {
                        continue;
                    }
                    while (allValues.hasMoreElements()) {
                        String value = StringUtil.toString(allValues.nextElement());
                        attributes.put(attributeName, value);
                        valueValid = true;
                    }
                }
                if (valueValid) {
                    attributesList.add(attributes);
                }
            }
        } catch (LDAPException e) {
            log.error("getAttributesFromSearchResults error=" + e, e);
        }
        return attributesList;
    }

    public static Boolean checkLdapDnAndPassword(LdapLoginBean ldapLoginBean, String userDn, String password) {
        LDAPConnection ldapConnection = null;
        Integer port = ldapLoginBean.getPort();
        if (BooleanUtil.isTrue(ldapLoginBean.getIsSsl())) {
            ldapConnection = new LDAPConnection(new LDAPJSSESecureSocketFactory());
            if (null == port) {
                port = LDAPConnection.DEFAULT_SSL_PORT;
            }
        } else {
            ldapConnection = new LDAPConnection();
            if (null == port) {
                port = LDAPConnection.DEFAULT_PORT;
            }
        }
        try {
            ldapConnection.connect(ldapLoginBean.getServerName(), port);
            ldapConnection.bind(LDAPConnection.LDAP_V3, userDn, password.getBytes("UTF8"));
        } catch (Throwable e) {
            log.error("checkLdapDnAndPassword error=" + e, e);
            return false;
        } finally {
            disconnectLdapConnection(ldapConnection);
        }
        return true;
    }
}